#!/usr/bin/python
# coding:utf-8
# Copyright 2017, JAKA
# All rights reserved.
import json
import os
import re
import shutil
import sys
import traceback
from enum import Enum
from collections import OrderedDict

# global controller_error_num, scb_error_num, servo_error_num, extio_error_num, joint_count
# controller_error_num = 0
# scb_error_num = 0
# servo_error_num = 0
# extio_error_num = 0
# joint_count = 0
CHECKHEADER = False

import _locale

_locale._getdefaultlocale = lambda *args: ["zh_CN", "utf8"]

root_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../../../../"))
all_lan = ["zh", "en", "ja", "vi", "fr"]
# target_dir = os.path.join(root_dir, 'scripts/log/events.rules')
# md_target_dir = os.path.join(root_dir, 'docs/ecode')
target_dir = os.path.join(root_dir, "scripts/log/events.rules")
md_target_dir = os.path.join(root_dir, "docs/ecode")
error_header = os.path.join(root_dir, "src/errcode.h")
shutil.rmtree(target_dir, True)
shutil.rmtree(md_target_dir, True)


class JakaError(object):
    """
    根据错误码定义文档构建错误类
    """

    class ErrSource(Enum):
        CONTROLLER = 0
        SERVO = 1
        RIO_OR_EXTIO = 2
        SCB_OR_TOR_SENSOR = 3

    class ErrLevel(Enum):
        error = 0
        info = 1

    def __init__(self, errcode):
        """

        Args:
            errcode: int
        """
        self.errcode = errcode
        self.errcode_str = "0x{:08X}".format(errcode)
        self.level = (errcode & 0x0F000000) >> 24
        self.source = (errcode & 0x00F00000) >> 20
        self.joint = (errcode & 0x000F0000) >> 16
        self.real_err = errcode & 0x0000FFFF

    def same_error_on_joint(self, joint_id, fill_bits=6):
        errorcode = hex((self.errcode & 0xFFF0FFFF) + ((joint_id & 0xF) << 16))[2:].upper()  # 应当补全到8位,为了适配app代码，补全到6位
        if errorcode.endswith("L"):
            errorcode = errorcode[:-1]
        errorcode = "0x" + errorcode.zfill(fill_bits).upper()
        return errorcode
        # return hex((self.errcode & 0xFFF0FFFF) + ((joint_id & 0xF) << 16))


class RobotErrorFromHeader:
    """
    实现从errcode.h读取所有定义
    """

    # todo 余留实现从errorcode.h读取所有定义，与生成的json对比，检查是否有缺失
    def __init__(self, c_error_file="errcode.h"):
        self._c_error_file = c_error_file
        self.all_err = self._extract_macros_from_header()
        self.all_err_num = len(self.all_err)

    def _extract_macros_from_header(self):
        """

        Returns:一个由tuples 构成的list，tuple[0]为errname,type=str;tuple[1]为errcode,type=str
            Examples:[('ROBOT_POWERED_OFF', '0x000F0001')]
        """
        macros = []
        with open(self._c_error_file, "r") as file:
            lines = file.readlines()
            pattern = r"#\s*define\s+(\w+)\s+(.*)"
            multi_line_macro = None
            for line in lines:
                if multi_line_macro:
                    # 如果存在跨行的宏定义，将其与当前行合并
                    multi_line_macro += line.strip()
                    match = re.match(pattern, multi_line_macro)
                    if match:
                        macro_name = match.group(1)
                        macro_value = match.group(2)
                        macros.append((macro_name, "0x{:08X}".format(int(macro_value.split()[0], 16))))
                        multi_line_macro = None
                    continue
                match = re.match(pattern, line)
                if match:
                    macro_name = match.group(1)
                    macro_value = match.group(2)
                    if "\\" in macro_value:  # 检查是否是跨行宏定义
                        multi_line_macro = line.strip().replace("\\", "")
                    else:
                        if macro_value != "":
                            macros.append((macro_name, "0x{:08X}".format(int(macro_value.split()[0], 16))))
        return macros

    def get_error_from_source_type(self, source_type):
        if source_type not in JakaError.ErrSource._value2member_map_:
            raise ValueError("source_type should be in Enum ErrSource")
        err_list = []
        for errname, errcode in self.all_err:
            if JakaError(int(errcode, 16)).source == source_type:
                err_list.append(errcode)
        return err_list


class SingleRobotErrorJsonParser:

    def __init__(self, json_file_path, all_languages, langueage_translate_check=True):
        """

        Args:
            json_file_path: 单个json错误码
            all_languages: 语言列表
            langueage_translate_check: Default True, check if detail values is ""
        """
        self.langueage_translate_check = langueage_translate_check
        self._file_path = json_file_path
        self._all_languages = all_languages
        self.error_detail_keys = ["reason", "action", "tittle", "description"]
        self.type = None
        self.isShowConfirm = None
        self.isShowOTA = None
        self.detail = {i: None for i in self._all_languages}
        # todo 校验json结构
        self.json_data = self._read_json()
        self.current_error = JakaError(int(self.json_data["errorcode"], 16))
        # self.fixParamList = ["0","1","2","3","4"]
        self.MaxParamNum = 20
        self.fixParamList = [("{" + str(x) + "}") for x in range(self.MaxParamNum)]

    def check_json_values(self, json_data):
        def is_value_str_or_unicode(value):
            ret = isinstance(value, str)
            return ret

        # punc = "！？｡＂＃＄％＆＇（）＊＋，－／：；＜＝＞＠［＼］＾＿｀｛｜｝～｟｠｢｣､、〃》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘’‛“”„‟…‧﹏."
        punc = "＂〃〝〞〟‘’‛“”„‟"
        regex_cn_punctuation = r"([{}])+".format(re.escape(punc))
        regex_hierarchical_numbering = r"\d+、"
        for key, value in json_data.items():
            if is_value_str_or_unicode(value):
                if re.search(regex_hierarchical_numbering, value):
                    raise ValueError(
                        "[VALUE ERR] Value: '{}' at key '{}' contains a number followed by '、'. Please use '1.' '1.1' for hierarchical numbering.".format(
                            value, key
                        )
                    )
                if re.search(regex_cn_punctuation, value):  # 检查 值 是否含有"num + 、"
                    print("!!!matched", re.search(regex_cn_punctuation, value).group())
                    raise ValueError("[VALUE ERR] Value: '{}' at key '{}' 部分中文标点符号被禁用".format(value, key))
            elif isinstance(value, dict):
                self.check_json_values(value)

    def _read_json(self):
        json_data = {}
        with open(self._file_path, "r") as json_file:
            try:
                content = json_file.read()
                json_data = json.loads(content)
                self.check_json_values(json_data)
            except Exception as err:
                print(traceback.format_exc())
                print("err occur when reading file {}".format(self._file_path))
                raise err
            if "errorcode" not in json_data.keys():
                raise ValueError("cannot find key 'errorcode' ,filename:{}".format(self._file_path))
            if json_data["type"]:
                self.type = int(json_data["type"])
            else:
                raise ValueError("type cannot be '' or None ,filename:{}".format(self._file_path))
            if json_data["isShowConfirm"]:
                self.isShowConfirm = json_data["isShowConfirm"]
            else:
                raise ValueError("isShowConfirm cannot be '' or None ,filename:{}".format(self._file_path))
            if json_data["isShowOTA"]:
                self.isShowOTA = json_data["isShowOTA"]
            else:
                raise ValueError("isShowOTA cannot be '' or None ,filename:{}".format(self._file_path))
            for lan in self._all_languages:
                if (set(self.error_detail_keys) - set(json_data[lan].keys())) != set():
                    print(json_data[lan].keys())
                    print(self.error_detail_keys)
                    print("lost key: {}".format(set(self.error_detail_keys) - set(json_data[lan].keys())))
                    raise KeyError("KEY lost in lan:{}\n filename:{}".format(lan, self._file_path))
                self.detail[lan] = json_data[lan]
                for key in json_data[lan].keys():
                    if self.langueage_translate_check:
                        if json_data[lan][key] != "" or json_data["zh"][key] == "":
                            pass
                        else:
                            raise ValueError("{} cannot be '' or None in language {}\nfilename:{}".format(key, lan, self._file_path))
        return json_data

    def get_error_data(self, language):
        """

        Args:
            language: str 需要错误码json中有该语言的key

        Returns:
            dict: key: strings errorcode
                value: dict detail description of errorcode
        """
        # global controller_error_num, scb_error_num, servo_error_num, extio_error_num, joint_count
        ret_json = {}

        # 应当补全到8位, 但适配app的代码，补全到6位，grpc的错误码补全到5位
        if self.current_error.level == JakaError.ErrSource.CONTROLLER.value and self.current_error.joint == 0xE:
            fill_bits = 5
        else:
            fill_bits = 6
        ecode = self.current_error.same_error_on_joint(self.current_error.joint, fill_bits)
        # if language == "zh":
        #     if current_error.source == 0:
        #         controller_error_num = controller_error_num + 1
        #         if "controller" not in self.file_path and "grpc" not in self.file_path:
        #             print(self.file_path)
        #     if current_error.source == 1:
        #         servo_error_num = servo_error_num + 1
        #     if current_error.source == 2:
        #         extio_error_num = extio_error_num + 1
        #     if current_error.source == 3:
        #         scb_error_num = scb_error_num + 1
        ota = self.json_data["isShowOTA"]
        etype = self.json_data["type"]
        confirm = self.json_data["isShowConfirm"]
        if "format" in self.json_data and self.json_data["format"] == True:
            joint_num = int(self.json_data["jointnum"])
            # if language == "zh":
            #     joint_count = joint_count + 1
            for i in range(joint_num):
                formatDict = {"jointnum": i + 1}
                try:
                    ret_json[self.current_error.same_error_on_joint(i)] = {
                        "isShowOTA": ota,
                        "type": etype,
                        "isShowConfirm": confirm,
                        "tittle": self.json_data[language]["tittle"].format(*self.fixParamList, **formatDict),
                        "description": self.json_data[language]["description"].format(*self.fixParamList, **formatDict),
                        "reason": self.json_data[language]["reason"].format(*self.fixParamList, **formatDict),
                        "action": self.json_data[language]["action"],
                    }
                    if "param" in self.json_data[language]:
                        ret_json[self.current_error.same_error_on_joint(i)]["param"] = self.json_data[language]["param"]
                except Exception as e:
                    print("处理单个错误文件时异常 文件名为{}".format(self._file_path))
                    raise e

        else:
            ret_json[ecode] = {
                "isShowOTA": ota,
                "type": etype,
                "isShowConfirm": confirm,
                "tittle": self.json_data[language]["tittle"],
                "description": self.json_data[language]["description"],
                "reason": self.json_data[language]["reason"],
                "action": self.json_data[language]["action"],
            }
            if "param" in self.json_data[language]:
                ret_json[ecode]["param"] = self.json_data[language]["param"]
        return ret_json


class MyJson2Md:

    class ErrorType(Enum):
        type_err = 0
        type_warn = 1
        type_info = 2

    md_tag = {
        "zh": {
            "type": {
                "name": "类型",
                ErrorType["type_err"].value: "错误",
                ErrorType["type_warn"].value: "警告",
                ErrorType["type_info"].value: "信息",
            },
            "confirm": {
                "name": "是否确认弹窗",
                1: "是",
                0: "否",
            },
            "description": "信息描述",
            "reason": "原因",
            "action": "解决措施",
        },
        "en": {
            "type": {
                "name": "Type",
                ErrorType["type_err"].value: "Error",
                ErrorType["type_warn"].value: "Warning",
                ErrorType["type_info"].value: "Info",
            },
            "confirm": {
                "name": "Is show confirm",
                1: "Yes",
                0: "No",
            },
            "description": "Description",
            "reason": "Reason",
            "action": "Action",
        },
        "ja": {
            "type": {
                "name": "Type",
                ErrorType["type_err"].value: "Error",
                ErrorType["type_warn"].value: "Warning",
                ErrorType["type_info"].value: "Info",
            },
            "confirm": {
                "name": "Is show confirm",
                1: "Yes",
                0: "No",
            },
            "description": "Description",
            "reason": "Reason",
            "action": "Action",
        },
        "vi": {
            "type": {
                "name": "Type",
                ErrorType["type_err"].value: "Error",
                ErrorType["type_warn"].value: "Warning",
                ErrorType["type_info"].value: "Info",
            },
            "confirm": {
                "name": "Is show confirm",
                1: "Yes",
                0: "No",
            },
            "description": "Description",
            "reason": "Reason",
            "action": "Action",
        },
        "fr": {
            "type": {
                "name": "Type",
                ErrorType["type_err"].value: "Error",
                ErrorType["type_warn"].value: "Warning",
                ErrorType["type_info"].value: "Info",
            },
            "confirm": {
                "name": "Is show confirm",
                1: "Yes",
                0: "No",
            },
            "description": "Description",
            "reason": "Reason",
            "action": "Action",
        },
    }

    def __init__(self, json_file_path, language):
        """

        Args:
            json_file_path: 生成的APP使用的json文件
            language: 语言
        """
        self.file_path = json_file_path
        self.json_data = self._read_json()
        self.language = language  # todo 检查language是否在md_tag的语言中
        self.md_str = self._gen_md_from_json()

    def _read_json(self):
        with open(self.file_path, "r") as json_file:
            json_data = json.load(json_file)
            ordered_dict = OrderedDict(json_data)
            sorted_dict = OrderedDict(sorted(ordered_dict.items(), key=lambda x: int(x[0], 16)))
        return sorted_dict

    def _single_errcode_json_to_md(self, errcode, single_json):
        ecode = errcode
        ota = single_json["isShowOTA"]
        etype = int(single_json["type"])
        confirm = int(single_json["isShowConfirm"])
        etype_lan = self.md_tag[self.language]["type"][etype]
        confirm_lan = self.md_tag[self.language]["confirm"][confirm]
        md1 = "## {} {}\n\n".format(ecode, single_json["tittle"].encode("utf-8"))
        md2 = "{}:{}\n".format(self.md_tag[self.language]["type"]["name"], etype_lan)
        md3 = "{}:{}\n\n".format(self.md_tag[self.language]["confirm"]["name"], confirm_lan)
        md4 = "### {}\n\n{}\n\n".format(self.md_tag[self.language]["description"], single_json["description"].encode("utf-8"))
        md5 = "### {}\n\n{}\n\n".format(self.md_tag[self.language]["reason"], single_json["reason"].encode("utf-8"))
        md6 = "### {}\n\n{}\n\n".format(self.md_tag[self.language]["action"], single_json["action"].encode("utf-8"))
        ret_md = md1 + md2 + md3 + md4 + md5 + md6
        return ret_md

    def _gen_md_from_json(self):
        md_str = ""
        for i in self.json_data:
            md_str = md_str + self._single_errcode_json_to_md(i, self.json_data[i])
        return md_str

    def get_md_str(self):
        return self.md_str

    def save_md_str(self, file_path):
        """

        Args:
            file_path: 保存到文件路径

        Returns:

        """
        try:
            with open(file_path, "w") as file:
                file.write(self.md_str)
                print("文件已保存至: {}".format(file_path))
        except Exception as e:
            print("保存文件时出现错误：{}".format(e))


def get_json_file_list(dir_path):
    """

    Args:
        dir_path: folder

    Returns:
        json_files_list: 文件夹下的所有json文件绝对路径
    """
    json_file_list = []
    for root, dirs, files in os.walk(dir_path):
        dirs[:] = [d for d in dirs if not d.startswith(".")]
        for file in files:
            if os.path.splitext(file)[1] == ".json":
                json_file_list.append(os.path.join(root, file))
    return json_file_list


def create_lan_json(all_lan, single_err_files):
    """

    Args:
        all_lan: list of str
        single_err_files: list of paths of single error.json

    Returns:

    """
    print("errcode_files_num:", len(single_err_files))
    os.makedirs(target_dir)
    lan_json = {}
    file_dict = {}
    err_files = []
    all_err_from_json = {}
    for lan in all_lan:
        lan_json[lan] = {}
        file_dict[lan] = ""
        all_err_from_json[lan] = []

    for f in single_err_files:
        try:
            parser = SingleRobotErrorJsonParser(f, all_lan)
        except ValueError as err:
            print(err)
            err_files.append(f)
            continue
        for lan in all_lan:
            ret = parser.get_error_data(lan)
            all_err_from_json[lan].extend(["0x{:08X}".format(int(x, 16)) for x in ret.keys()])
            if set(ret.keys()) <= set(lan_json[lan].keys()):
                raise ValueError("repeat Errcode detected!\n errcode is {}\nerr file is {}".format(ret.keys(), f))
            lan_json[lan].update(ret)
    if len(err_files) > 0:
        print("err file num:{}".format(len(err_files)))
        print(err_files)
        raise ValueError("JSON files format is not correct")
    if CHECKHEADER:
        all_err_from_head = RobotErrorFromHeader(error_header)
        all_int_err_from_head = [x[1] for x in all_err_from_head.all_err]
        set_head = set(all_int_err_from_head)
        for lan in all_lan:
            set_err_from_json = set(all_err_from_json[lan])
            diff_err = set_head - set_err_from_json
            if len(diff_err) > 0:
                print("{} 存在一些json中没有但head中有的errocode:".format(lan))
                print(diff_err)
            diff_err = set_err_from_json - set_head
            if len(diff_err) > 0:
                print("{} 存在一些head中没有但json中有的errocode:".format(lan))
                print(diff_err)
    for lan in lan_json:
        file_dict[lan] = os.path.join(target_dir, lan + ".json")
        # 按照错误码的int值进行排序
        ordered_dict = OrderedDict(lan_json[lan])
        sorted_dict = OrderedDict(sorted(ordered_dict.items(), key=lambda x: int(x[0], 16)))
        print("err_codes_num:", len(lan_json[lan]), "language:", lan)
        # 写入语言文件
        with open(os.path.join(target_dir, lan + ".json"), "w") as lan_file:
            json.dump(sorted_dict, lan_file, ensure_ascii=False, sort_keys=False, indent=4)
    for i in file_dict:
        print("文件已保存至: {}".format(file_dict[i]))
    print("json gen finished ")
    return file_dict


def create_lan_markdown(err_file_dict):
    """

    Args:
        err_file_dict: {language: filepath}
            example:{"zh": "./zh.json"}

    Returns:

    """
    shutil.rmtree(md_target_dir, True)
    os.makedirs(md_target_dir)
    for lan in err_file_dict:
        parser = MyJson2Md(err_file_dict[lan], lan)
        parser.save_md_str(os.path.join(md_target_dir, lan + ".md"))


if __name__ == "__main__":
    required_languages = ["zh", "en", "ja", "vi", "fr"]
    json_file_list = get_json_file_list(os.path.dirname(__file__))
    print(len(json_file_list))
    json_dict = create_lan_json(required_languages, json_file_list)
    create_lan_markdown(json_dict)
    pass
